-
Notifications
You must be signed in to change notification settings - Fork 3
Created a custom rule for import-type-order #48
Conversation
26a470b
to
73d9b7a
Compare
} | ||
} | ||
|
||
function getImportType(path: string): ImportType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any suggestions on making this look a little cleaner/prettier?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, I'd go the other way and make it uglier. It's not going to expand beyond 4 possibilities, and most devs grok these hardcoded strings faster than pretty method names.
if (path.substr(1, 2) === './') {
return Import.Sibling;
} else if (path.substr(1, 4) === '../') {
If I was going to make it prettier, I'd try adding a static toType(path: string)
function to the enum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start! Lots of comments, but nothing major.
} | ||
|
||
// e.g. 'import Foo from './foo';' | ||
public visitImportDeclaration(node: ts.ImportDeclaration) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So normally this would be cool, but I'm really concerned about performance. yarn check
is taking over 2 minutes to run (>40% of CI time), and that'll only get worse as we add more code + rules. AFAIK, there's no way to parallelize tslint.
Taking a look at the tslint performance guide, it recommends:
- Don’t walk the whole AST if possible
- Implement your own walking algorithm
If you are interested in import statements for example, you only need to search in sourceFile.statements and nested NamespaceDeclaration
... and since nested declarations can't do module imports, that limits this rule to sourceFile.statements
.
Here's a rough version of the above, with annotations on how to improve it. It passes for the existing tests, at least.
// This should be adapted to `extend Lint.AbstractWalker<MyOptionsType>`, and implement the `walk` rule.
class StructuredImportsWalker extends Lint.RuleWalker {
private previousImport: ImportType;
private currentImport: ImportType;
constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
super(sourceFile, options);
}
public visitSourceFile(node: ts.SourceFile) {
const importNodes = node.statements
.filter((child) => child.kind === ts.SyntaxKind.ImportDeclaration)
.map((child) => child as ts.ImportDeclaration);
if (importNodes.length === 0) { return; }
// This is off the top of my head; probably a more readable way to do this.
let lastGoodType = getImportType(importNodes.shift());
while (importNodes.length) {
const currentImport = importNodes.shift();
const currentType = getImportType(currentImport);
// Here we're just relying on enum comparisons. This gets rid of a couple of helper functions.
if (lastGoodType > currentType) {
this.addFailure(
this.createFailure(
currentImport.moduleSpecifier.getStart(),
currentImport.moduleSpecifier.getFullWidth(),
Rule.STRUCTURED_IMPORTS_ABS_FIRST_ERROR,
)
);
} else {
lastGoodType = currentType;
}
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback!
I couldn't run my tests with this, earlier today. It needed a bit more refactoring but it was a great starting point.
Did you build then run the tests? I think that might of done it, it gets me all the time too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I was using this to run the test:
yarn run build && $(npm bin)/tslint -r ./build/customRules/ --test test/rules/structured-imports/
The above code was a tweaked version of this ugly stuff (that definitely passes)... I'll hide this in a details block:
import * as ts from 'typescript'; import * as Lint from 'tslint';export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys /
public static metadata: Lint.IRuleMetadata = {
'ruleName': 'structured-imports',
'description': 'Enforce structure to your imports. Import structure should be listed in the folloing order: modules, absolute imports, relative parent/ancestor directories, relative sibling directors.',
'hasFix': false,
'optionsDescription': 'Not configurable.',
'options': null,
'optionExamples': null,
'type': 'style',
'typescriptOnly': false,
};
/ tslint:enable:object-literal-sort-keys */public static STRUCTURED_IMPORTS_ABS_FIRST_ERROR = 'Imports should be listed in the following order: module imports, absolute imports, relative imports.';
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const structuredImportsWalker = new StructuredImportsWalker(sourceFile, this.getOptions());
return this.applyWithWalker(structuredImportsWalker);
}
}enum ImportType {
Module = 0, // 'module'
Absolute = 1, // '/some/folder/file'
RelativeAncestor = 2, // '../parentFolder'
RelativeSibling = 3, // './siblingFolder'
};const importStuctureOrder = [ImportType.Module, ImportType.Absolute, ImportType.RelativeAncestor, ImportType.RelativeSibling];
class StructuredImportsWalker extends Lint.RuleWalker {
private previousImport: ImportType;
private currentImport: ImportType;constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
super(sourceFile, options);
}public visitSourceFile(node: ts.SourceFile) {
const importNodes = node.statements
.filter((child) => child.kind === ts.SyntaxKind.ImportDeclaration)
.map((child) => child as ts.ImportDeclaration);if (importNodes.length === 0) { return; } let lastGoodType = getImportType(importNodes.shift()); while (importNodes.length) { const currentImport = importNodes.shift(); const currentType = getImportType(currentImport); if (lastGoodType > currentType) { this.addFailure( this.createFailure( currentImport.moduleSpecifier.getStart(), currentImport.moduleSpecifier.getFullWidth(), Rule.STRUCTURED_IMPORTS_ABS_FIRST_ERROR, ) ); } else { lastGoodType = currentType; } }
}
}function getImportType({moduleSpecifier}: ts.ImportDeclaration): ImportType {
const path = moduleSpecifier.getText();
if (isRelativeSibling(path)) {
return ImportType.RelativeSibling;
} else if (isRelativeAncestor(path)) {
return ImportType.RelativeAncestor;
} else if (isAbsolute(path)) {
return ImportType.Absolute;
}
return ImportType.Module;
}function isAbsolute(path: string) {
return path[1] === '/' ; // [0] is quote mark
}function isRelativeAncestor(path: string) {
return path.substr(1, 4) === '../' ; // path[0] is quote mark
}function isRelativeSibling(path: string) {
return path.substr(1, 2) === './' ; // path[0] is quote mark
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, must of done something wrong when I was trying this our earlier today.
docs/rules/structured-imports.md
Outdated
# Enforces a certain structure in your import statements | ||
|
||
## Rationale | ||
- Consistency in import statement structure throughout the codebase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This states a Shopify-specific end goal rather than a rationale. Looking through other rationales, adjacent-overload-signature
has this:
Improves readability and organization by grouping naturally related items together.
That looks good to me.
docs/rules/structured-imports.md
Outdated
- Consistency in import statement structure throughout the codebase | ||
|
||
## Rule Details | ||
Enforce a certain structure in your imports. Import structure should be listed in the following order: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this is vague/flowery.
Imports should be in the following order:
* Externals
* Absolute paths
* Parent directories
* Siblings
Note that I'm borrowing "externals" from the eslint import/order
rule. Everything in TypeScript is a module, so a more specific term is useful.
docs/rules/structured-imports.md
Outdated
import BarModule from 'BarModule'; | ||
``` | ||
|
||
The following patterns are not warnings: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer: The following import order is valid:
Sometimes coding rules apply to English! Less cognitive load to interpret "valid" than "not warnings".
docs/rules/structured-imports.md
Outdated
|
||
## When Not To Use It | ||
|
||
If you do not with to enforce consistency in your import structuring. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: with
=>wish
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
structuring
is flowery; the sentence reads the same without it.
@@ -0,0 +1,10 @@ | |||
import Foo from './foo'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I've no idea what multiLine
is testing. This rule will never fail on a single import ¯\(ツ)/¯
'options': null, | ||
'optionExamples': null, | ||
'type': 'style', | ||
'typescriptOnly': false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't care about JS, so let's make this true
.
/* tslint:disable:object-literal-sort-keys */ | ||
public static metadata: Lint.IRuleMetadata = { | ||
'ruleName': 'structured-imports', | ||
'description': 'Enforce structure to your imports. Import structure should be listed in the folloing order: modules, absolute imports, relative parent/ancestor directories, relative sibling directors.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: folloing
=>following
.
enum ImportType { | ||
Module = 0, // 'module' | ||
Absolute = 1, // '/some/folder/file' | ||
RelativeAncestor = 2, // '../parentFolder' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RelativeAncestor
=>Ancestor
(relative is implied).
Module = 0, // 'module' | ||
Absolute = 1, // '/some/folder/file' | ||
RelativeAncestor = 2, // '../parentFolder' | ||
RelativeSibling = 3, // './siblingFolder' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RelativeSibling
=>Sibling
.
const importStuctureOrder = [ImportType.External, ImportType.Absolute, ImportType.Ancestor, ImportType.Sibling]; | ||
|
||
class StructuredImportsWalker extends Lint.AbstractWalker<void> { | ||
private previousImport: ImportType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the benefit of making the walker stateful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woah, that was fast!
There's no benefit to it being stateful. I was still refactoring, just pushed whatever I had that was in a 'happy state'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, right! Happy states are good practice. Sorry, too quick on the trigger 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No no, its okay. I appreciate the helpful comments more than you think!
3d82bcf
to
408a9a0
Compare
@@ -57,7 +57,7 @@ function walker(context: Lint.WalkContext<void>): void { | |||
function getImportType(path: string): ImportType { | |||
if (path.substr(1, 2) === './') { | |||
return ImportType.Sibling; | |||
} else if (path.substr(1, 4) === '../') { | |||
} else if (path.substr(1, 3) === '../') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The granular test cases are doing their job! 👍
@GoodForOneFare All the changes are addressed. Have another look and let me know what you think. I've created an issue for creating custom Rule fixes. I'll add those as separate PR's in the near future, so we can keep things moving with this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just minor comments. With some quick cleanup, we'll be able to ship this today 🎉
@@ -0,0 +1,4 @@ | |||
import {Foo, Bar, Baz} from 'FooBarBaz'; | |||
import BazTwo from '/Baz'; | |||
import FooTwo from '../foo'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a side-effecty import somewhere in here, please?
docs/rules/import-type-order.md
Outdated
- Improves readability and organization by grouping naturally related items together. | ||
|
||
## Rule Details | ||
Enforce order of import statement types. Import structure should be listed in the following order: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
structure
isn't adding anything to this sentence.
docs/rules/import-type-order.md
Outdated
|
||
The following are considered warnings | ||
```js | ||
import Foo from './foo'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add some comments explaining the nature of each offence. e.g.,
import Foo from './foo'; // Should come after external and parent imports.
enum ImportType { | ||
External = 0, // 'external' | ||
Absolute = 1, // '/some/folder/file' | ||
Ancestor = 2, // '../parentFolder' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still on the fence about Ancestor
, but I can't think of anything better. Internal
? Local
? No. Bah.
Ohwell, they're not used outside this file, so we can easily change when inspiration strikes 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's annoying because parents and ancestors are both valid imports for this type. I think Ancestor
is the best name, given all our options.
|
||
const importStuctureOrder = [ImportType.External, ImportType.Absolute, ImportType.Ancestor, ImportType.Sibling]; | ||
|
||
function walker(context: Lint.WalkContext<void>): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
} | ||
} | ||
|
||
function getImportType(path: string): ImportType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just accept getImportType({moduleSpecifier}: ts.ImportDeclaration)
, and fish out the text in here.
It's a minor reduction in the walker's verbosity, but every little bit helps.
|
||
let previousImport = getImportType(importNodes.shift().moduleSpecifier.getText()); | ||
while (importNodes.length) { | ||
const currentImportNode = importNodes.shift().moduleSpecifier; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moduleSpecifier
is of type StringLiteral
, so this should be currentPath
or currentModuleSpecifier
.
let previousImport = getImportType(importNodes.shift().moduleSpecifier.getText()); | ||
while (importNodes.length) { | ||
const currentImportNode = importNodes.shift().moduleSpecifier; | ||
const currentImport = getImportType(currentImportNode.getText()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currentType
would be more accurate.
|
||
if (importNodes.length === 0) { return; } | ||
|
||
let previousImport = getImportType(importNodes.shift().moduleSpecifier.getText()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previous
implies that the loop is just dumbly assigning to this var, but it's only assigning types for valid imports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... and it's not an import. It's a lastValidType
.
A |
@GoodForOneFare I think we're good now! 🤞 |
Ewww. Thought I'd give the GitHub web ui a shot for fixing merge conflicts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix more pedantry and 🚢 👍
docs/rules/import-type-order.md
Outdated
|
||
```js | ||
export interface TheThreeStooges { | ||
import BarModule from 'BarModule'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imports should be indented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, I don't even know why I wrapped those imports in an interface. Probably pulled code from somewhere and forgot to get rid of the export.
/* tslint:disable:object-literal-sort-keys */ | ||
public static metadata: Lint.IRuleMetadata = { | ||
'ruleName': 'import-type-order', | ||
'description': 'Enforce structure to your imports. Import structure should be listed in the following order: modules, absolute imports, relative parent/ancestor directories, relative sibling directors.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improves readability and organization by grouping related imports together. Imports should be listed in order of: external modules, absolute paths, relative paths, relative siblings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should've been fixed.
}; | ||
/* tslint:enable:object-literal-sort-keys */ | ||
|
||
public static IMPORT_TYPE_ORDER_ERROR = 'Imports should be listed in the following order: module imports, absolute imports, ancestor imports, sibling imports.'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/module/external
0751d80
to
f1a2311
Compare
f1a2311
to
97f6669
Compare
Enforce a certain structure in your imports. Import structure should be listed in the following order:
Issue #42